/*
 * Copyright 2010-2025 Hyland Software, Inc. and its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.activiti.engine.impl.history;

import java.util.Date;
import java.util.List;
import org.activiti.bpmn.model.FlowElement;
import org.activiti.bpmn.model.FlowNode;
import org.activiti.bpmn.model.SequenceFlow;
import org.activiti.engine.delegate.event.ActivitiEventDispatcher;
import org.activiti.engine.delegate.event.ActivitiEventType;
import org.activiti.engine.delegate.event.impl.ActivitiEventBuilder;
import org.activiti.engine.impl.cfg.IdGenerator;
import org.activiti.engine.impl.cfg.ProcessEngineConfigurationImpl;
import org.activiti.engine.impl.identity.Authentication;
import org.activiti.engine.impl.persistence.AbstractManager;
import org.activiti.engine.impl.persistence.cache.EntityCache;
import org.activiti.engine.impl.persistence.entity.CommentEntity;
import org.activiti.engine.impl.persistence.entity.ExecutionEntity;
import org.activiti.engine.impl.persistence.entity.HistoricActivityInstanceEntity;
import org.activiti.engine.impl.persistence.entity.HistoricDetailVariableInstanceUpdateEntity;
import org.activiti.engine.impl.persistence.entity.HistoricIdentityLinkEntity;
import org.activiti.engine.impl.persistence.entity.HistoricProcessInstanceEntity;
import org.activiti.engine.impl.persistence.entity.HistoricTaskInstanceEntity;
import org.activiti.engine.impl.persistence.entity.HistoricVariableInstanceEntity;
import org.activiti.engine.impl.persistence.entity.IdentityLinkEntity;
import org.activiti.engine.impl.persistence.entity.TaskEntity;
import org.activiti.engine.impl.persistence.entity.VariableInstanceEntity;
import org.activiti.engine.task.Event;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Manager class that centralises recording of all history-related operations that are originated from inside the engine.
 *
 */
public class DefaultHistoryManager extends AbstractManager implements HistoryManager {

    private static Logger log = LoggerFactory.getLogger(DefaultHistoryManager.class.getName());

    private HistoryLevel historyLevel;

    public DefaultHistoryManager(ProcessEngineConfigurationImpl processEngineConfiguration, HistoryLevel historyLevel) {
        super(processEngineConfiguration);
        this.historyLevel = historyLevel;
    }

    /*
     * (non-Javadoc)
     *
     * @see org.activiti.engine.impl.history.HistoryManagerInterface# isHistoryLevelAtLeast(org.activiti.engine.impl.history.HistoryLevel)
     */
    @Override
    public boolean isHistoryLevelAtLeast(HistoryLevel level) {
        if (log.isDebugEnabled()) {
            log.debug("Current history level: {}, level required: {}", historyLevel, level);
        }
        // Comparing enums actually compares the location of values declared in
        // the enum
        return historyLevel.isAtLeast(level);
    }

    /*
     * (non-Javadoc)
     *
     * @see org.activiti.engine.impl.history.HistoryManagerInterface#isHistoryEnabled ()
     */
    @Override
    public boolean isHistoryEnabled() {
        if (log.isDebugEnabled()) {
            log.debug("Current history level: {}", historyLevel);
        }
        return !historyLevel.equals(HistoryLevel.NONE);
    }

    // Process related history

    /*
     * (non-Javadoc)
     *
     * @see org.activiti.engine.impl.history.HistoryManagerInterface# recordProcessInstanceEnd(java.lang.String, java.lang.String, java.lang.String)
     */
    @Override
    public void recordProcessInstanceEnd(String processInstanceId, String deleteReason, String activityId) {
        if (isHistoryLevelAtLeast(HistoryLevel.ACTIVITY)) {
            HistoricProcessInstanceEntity historicProcessInstance = getHistoricProcessInstanceEntityManager().findById(
                processInstanceId
            );

            if (historicProcessInstance != null) {
                historicProcessInstance.markEnded(deleteReason);
                historicProcessInstance.setEndActivityId(activityId);

                // Fire event
                ActivitiEventDispatcher activitiEventDispatcher = getEventDispatcher();
                if (activitiEventDispatcher != null && activitiEventDispatcher.isEnabled()) {
                    activitiEventDispatcher.dispatchEvent(
                        ActivitiEventBuilder.createEntityEvent(
                            ActivitiEventType.HISTORIC_PROCESS_INSTANCE_ENDED,
                            historicProcessInstance
                        )
                    );
                }
            }
        }
    }

    @Override
    public void recordProcessInstanceNameChange(String processInstanceId, String newName) {
        if (isHistoryLevelAtLeast(HistoryLevel.ACTIVITY)) {
            HistoricProcessInstanceEntity historicProcessInstance = getHistoricProcessInstanceEntityManager().findById(
                processInstanceId
            );

            if (historicProcessInstance != null) {
                historicProcessInstance.setName(newName);
            }
        }
    }

    /*
     * (non-Javadoc)
     *
     * @see org.activiti.engine.impl.history.HistoryManagerInterface# recordProcessInstanceStart (org.activiti.engine.impl.persistence.entity.ExecutionEntity)
     */
    @Override
    public void recordProcessInstanceStart(ExecutionEntity processInstance, FlowElement startElement) {
        if (isHistoryLevelAtLeast(HistoryLevel.ACTIVITY)) {
            HistoricProcessInstanceEntity historicProcessInstance = getHistoricProcessInstanceEntityManager().create(
                processInstance
            );
            historicProcessInstance.setStartActivityId(startElement.getId());

            // Insert historic process-instance
            getHistoricProcessInstanceEntityManager().insert(historicProcessInstance, false);

            // Fire event
            ActivitiEventDispatcher activitiEventDispatcher = getEventDispatcher();
            if (activitiEventDispatcher != null && activitiEventDispatcher.isEnabled()) {
                activitiEventDispatcher.dispatchEvent(
                    ActivitiEventBuilder.createEntityEvent(
                        ActivitiEventType.HISTORIC_PROCESS_INSTANCE_CREATED,
                        historicProcessInstance
                    )
                );
            }
        }
    }

    /*
     * (non-Javadoc)
     *
     * @see org.activiti.engine.impl.history.HistoryManagerInterface# recordSubProcessInstanceStart (org.activiti.engine.impl.persistence.entity.ExecutionEntity,
     * org.activiti.engine.impl.persistence.entity.ExecutionEntity)
     */
    @Override
    public void recordSubProcessInstanceStart(
        ExecutionEntity parentExecution,
        ExecutionEntity subProcessInstance,
        FlowElement initialElement
    ) {
        if (isHistoryLevelAtLeast(HistoryLevel.ACTIVITY)) {
            HistoricProcessInstanceEntity historicProcessInstance = getHistoricProcessInstanceEntityManager().create(
                subProcessInstance
            );

            // Fix for ACT-1728: startActivityId not initialized with subprocess instance
            if (historicProcessInstance.getStartActivityId() == null) {
                historicProcessInstance.setStartActivityId(initialElement.getId());
            }
            getHistoricProcessInstanceEntityManager().insert(historicProcessInstance, false);

            // Fire event
            ActivitiEventDispatcher activitiEventDispatcher = getEventDispatcher();
            if (activitiEventDispatcher != null && activitiEventDispatcher.isEnabled()) {
                activitiEventDispatcher.dispatchEvent(
                    ActivitiEventBuilder.createEntityEvent(
                        ActivitiEventType.HISTORIC_PROCESS_INSTANCE_CREATED,
                        historicProcessInstance
                    )
                );
            }

            HistoricActivityInstanceEntity activitiyInstance = findActivityInstance(parentExecution, false, true);
            if (activitiyInstance != null) {
                activitiyInstance.setCalledProcessInstanceId(subProcessInstance.getProcessInstanceId());
            }
        }
    }

    // Activity related history

    /*
     * (non-Javadoc)
     *
     * @see org.activiti.engine.impl.history.HistoryManagerInterface#recordActivityStart (org.activiti.engine.impl.persistence.entity.ExecutionEntity)
     */
    @Override
    public void recordActivityStart(ExecutionEntity executionEntity) {
        if (isHistoryLevelAtLeast(HistoryLevel.ACTIVITY)) {
            if (executionEntity.getActivityId() != null && executionEntity.getCurrentFlowElement() != null) {
                HistoricActivityInstanceEntity historicActivityInstanceEntity = null;

                // Historic activity instance could have been created (but only in cache, never persisted)
                // for example when submitting form properties
                HistoricActivityInstanceEntity historicActivityInstanceEntityFromCache =
                    getHistoricActivityInstanceFromCache(
                        executionEntity.getId(),
                        executionEntity.getActivityId(),
                        true
                    );
                if (historicActivityInstanceEntityFromCache != null) {
                    historicActivityInstanceEntity = historicActivityInstanceEntityFromCache;
                } else {
                    historicActivityInstanceEntity = createHistoricActivityInstanceEntity(executionEntity);
                }

                // Fire event
                ActivitiEventDispatcher activitiEventDispatcher = getEventDispatcher();
                if (activitiEventDispatcher != null && activitiEventDispatcher.isEnabled()) {
                    activitiEventDispatcher.dispatchEvent(
                        ActivitiEventBuilder.createEntityEvent(
                            ActivitiEventType.HISTORIC_ACTIVITY_INSTANCE_CREATED,
                            historicActivityInstanceEntity
                        )
                    );
                }
            }
        }
    }

    /*
     * (non-Javadoc)
     *
     * @see org.activiti.engine.impl.history.HistoryManagerInterface#recordActivityEnd (org.activiti.engine.impl.persistence.entity.ExecutionEntity)
     */
    @Override
    public void recordActivityEnd(ExecutionEntity executionEntity, String deleteReason) {
        if (isHistoryLevelAtLeast(HistoryLevel.ACTIVITY)) {
            HistoricActivityInstanceEntity historicActivityInstance = findActivityInstance(
                executionEntity,
                false,
                true
            );
            if (historicActivityInstance != null) {
                historicActivityInstance.markEnded(deleteReason);

                // Fire event
                ActivitiEventDispatcher activitiEventDispatcher = getEventDispatcher();
                if (activitiEventDispatcher != null && activitiEventDispatcher.isEnabled()) {
                    activitiEventDispatcher.dispatchEvent(
                        ActivitiEventBuilder.createEntityEvent(
                            ActivitiEventType.HISTORIC_ACTIVITY_INSTANCE_ENDED,
                            historicActivityInstance
                        )
                    );
                }
            }
        }
    }

    @Override
    public HistoricActivityInstanceEntity findActivityInstance(
        ExecutionEntity execution,
        boolean createOnNotFound,
        boolean endTimeMustBeNull
    ) {
        String activityId = null;
        if (execution.getCurrentFlowElement() instanceof FlowNode) {
            activityId = execution.getCurrentFlowElement().getId();
        } else if (
            execution.getCurrentFlowElement() instanceof SequenceFlow && execution.getCurrentActivitiListener() == null
        ) {
            // while executing sequence flow listeners, we don't want historic activities
            activityId = ((SequenceFlow) (execution.getCurrentFlowElement())).getSourceFlowElement().getId();
        }

        if (activityId != null) {
            return findActivityInstance(execution, activityId, createOnNotFound, endTimeMustBeNull);
        }

        return null;
    }

    public HistoricActivityInstanceEntity findActivityInstance(
        ExecutionEntity execution,
        String activityId,
        boolean createOnNotFound,
        boolean endTimeMustBeNull
    ) {
        // No use looking for the HistoricActivityInstance when no activityId is provided.
        if (activityId == null) {
            return null;
        }

        String executionId = execution.getId();

        // Check the cache
        HistoricActivityInstanceEntity historicActivityInstanceEntityFromCache = getHistoricActivityInstanceFromCache(
            executionId,
            activityId,
            endTimeMustBeNull
        );
        if (historicActivityInstanceEntityFromCache != null) {
            return historicActivityInstanceEntityFromCache;
        }

        // If the execution was freshly created, there is no need to check the database,
        // there can never be an entry for a historic activity instance with this execution id.
        if (!execution.isInserted() && !execution.isProcessInstanceType()) {
            // Check the database
            List<HistoricActivityInstanceEntity> historicActivityInstances =
                getHistoricActivityInstanceEntityManager().findUnfinishedHistoricActivityInstancesByExecutionAndActivityId(
                    executionId,
                    activityId
                );

            if (historicActivityInstances.size() > 0) {
                return historicActivityInstances.get(0);
            }
        }

        if (execution.getParentId() != null) {
            HistoricActivityInstanceEntity historicActivityInstanceFromParent = findActivityInstance(
                (ExecutionEntity) execution.getParent(),
                activityId,
                false,
                endTimeMustBeNull
            ); // always false for create, we only check if it can be found
            if (historicActivityInstanceFromParent != null) {
                return historicActivityInstanceFromParent;
            }
        }

        if (
            createOnNotFound &&
            activityId != null &&
            ((execution.getCurrentFlowElement() != null && execution.getCurrentFlowElement() instanceof FlowNode) ||
                execution.getCurrentFlowElement() == null)
        ) {
            return createHistoricActivityInstanceEntity(execution);
        }

        return null;
    }

    protected HistoricActivityInstanceEntity getHistoricActivityInstanceFromCache(
        String executionId,
        String activityId,
        boolean endTimeMustBeNull
    ) {
        List<HistoricActivityInstanceEntity> cachedHistoricActivityInstances = getEntityCache().findInCache(
            HistoricActivityInstanceEntity.class
        );
        for (HistoricActivityInstanceEntity cachedHistoricActivityInstance : cachedHistoricActivityInstances) {
            if (
                activityId != null &&
                activityId.equals(cachedHistoricActivityInstance.getActivityId()) &&
                (!endTimeMustBeNull || cachedHistoricActivityInstance.getEndTime() == null)
            ) {
                if (executionId.equals(cachedHistoricActivityInstance.getExecutionId())) {
                    return cachedHistoricActivityInstance;
                }
            }
        }

        return null;
    }

    protected HistoricActivityInstanceEntity createHistoricActivityInstanceEntity(ExecutionEntity execution) {
        IdGenerator idGenerator = getProcessEngineConfiguration().getIdGenerator();

        String processDefinitionId = execution.getProcessDefinitionId();
        String processInstanceId = execution.getProcessInstanceId();

        HistoricActivityInstanceEntity historicActivityInstance = getHistoricActivityInstanceEntityManager().create();
        historicActivityInstance.setId(idGenerator.getNextId());
        historicActivityInstance.setProcessDefinitionId(processDefinitionId);
        historicActivityInstance.setProcessInstanceId(processInstanceId);
        historicActivityInstance.setExecutionId(execution.getId());
        historicActivityInstance.setActivityId(execution.getActivityId());
        if (execution.getCurrentFlowElement() != null) {
            historicActivityInstance.setActivityName(execution.getCurrentFlowElement().getName());
            historicActivityInstance.setActivityType(parseActivityType(execution.getCurrentFlowElement()));
        }
        Date now = getClock().getCurrentTime();
        historicActivityInstance.setStartTime(now);

        // Inherit tenant id (if applicable)
        if (execution.getTenantId() != null) {
            historicActivityInstance.setTenantId(execution.getTenantId());
        }

        getHistoricActivityInstanceEntityManager().insert(historicActivityInstance);
        return historicActivityInstance;
    }

    /*
     * (non-Javadoc)
     *
     * @see org.activiti.engine.impl.history.HistoryManagerInterface# recordProcessDefinitionChange(java.lang.String, java.lang.String)
     */
    @Override
    public void recordProcessDefinitionChange(String processInstanceId, String processDefinitionId) {
        if (isHistoryLevelAtLeast(HistoryLevel.ACTIVITY)) {
            HistoricProcessInstanceEntity historicProcessInstance = getHistoricProcessInstanceEntityManager().findById(
                processInstanceId
            );
            if (historicProcessInstance != null) {
                historicProcessInstance.setProcessDefinitionId(processDefinitionId);
            }
        }
    }

    // Task related history

    /*
     * (non-Javadoc)
     *
     * @see org.activiti.engine.impl.history.HistoryManagerInterface#recordTaskCreated (org.activiti.engine.impl.persistence.entity.TaskEntity,
     * org.activiti.engine.impl.persistence.entity.ExecutionEntity)
     */
    @Override
    public void recordTaskCreated(TaskEntity task, ExecutionEntity execution) {
        if (isHistoryLevelAtLeast(HistoryLevel.AUDIT)) {
            HistoricTaskInstanceEntity historicTaskInstance = getHistoricTaskInstanceEntityManager().create(
                task,
                execution
            );
            getHistoricTaskInstanceEntityManager().insert(historicTaskInstance, false);
        }

        recordTaskId(task);
    }

    /*
     * (non-Javadoc)
     *
     * @see org.activiti.engine.impl.history.HistoryManagerInterface#recordTaskAssignment (org.activiti.engine.impl.persistence.entity.TaskEntity)
     */
    @Override
    public void recordTaskAssignment(TaskEntity task) {
        ExecutionEntity executionEntity = task.getExecution();
        if (isHistoryLevelAtLeast(HistoryLevel.ACTIVITY)) {
            if (executionEntity != null) {
                HistoricActivityInstanceEntity historicActivityInstance = findActivityInstance(
                    executionEntity,
                    false,
                    true
                );
                if (historicActivityInstance != null) {
                    historicActivityInstance.setAssignee(task.getAssignee());
                }
            }
        }
    }

    /*
     * (non-Javadoc)
     *
     * @see org.activiti.engine.impl.history.HistoryManagerInterface#recordTaskClaim (org.activiti.engine.impl.persistence.entity.TaskEntity)
     */

    @Override
    public void recordTaskClaim(TaskEntity task) {
        if (isHistoryLevelAtLeast(HistoryLevel.AUDIT)) {
            HistoricTaskInstanceEntity historicTaskInstance = getHistoricTaskInstanceEntityManager().findById(
                task.getId()
            );
            if (historicTaskInstance != null) {
                historicTaskInstance.setClaimTime(task.getClaimTime());
            }
        }
    }

    /*
     * (non-Javadoc)
     *
     * @see org.activiti.engine.impl.history.HistoryManagerInterface#recordTaskId (org.activiti.engine.impl.persistence.entity.TaskEntity)
     */
    @Override
    public void recordTaskId(TaskEntity task) {
        if (isHistoryLevelAtLeast(HistoryLevel.ACTIVITY)) {
            ExecutionEntity execution = task.getExecution();
            if (execution != null) {
                HistoricActivityInstanceEntity historicActivityInstance = findActivityInstance(execution, false, true);
                if (historicActivityInstance != null) {
                    historicActivityInstance.setTaskId(task.getId());
                }
            }
        }
    }

    /*
     * (non-Javadoc)
     *
     * @see org.activiti.engine.impl.history.HistoryManagerInterface#recordTaskEnd (java.lang.String, java.lang.String)
     */
    @Override
    public void recordTaskEnd(String taskId, String deleteReason) {
        if (isHistoryLevelAtLeast(HistoryLevel.AUDIT)) {
            HistoricTaskInstanceEntity historicTaskInstance = getHistoricTaskInstanceEntityManager().findById(taskId);
            if (historicTaskInstance != null) {
                historicTaskInstance.markEnded(deleteReason);
            }
        }
    }

    /*
     * (non-Javadoc)
     *
     * @see org.activiti.engine.impl.history.HistoryManagerInterface# recordTaskAssigneeChange(java.lang.String, java.lang.String)
     */
    @Override
    public void recordTaskAssigneeChange(String taskId, String assignee) {
        if (isHistoryLevelAtLeast(HistoryLevel.AUDIT)) {
            HistoricTaskInstanceEntity historicTaskInstance = getHistoricTaskInstanceEntityManager().findById(taskId);
            if (historicTaskInstance != null) {
                historicTaskInstance.setAssignee(assignee);
            }
        }
    }

    /*
     * (non-Javadoc)
     *
     * @see org.activiti.engine.impl.history.HistoryManagerInterface# recordTaskOwnerChange(java.lang.String, java.lang.String)
     */
    @Override
    public void recordTaskOwnerChange(String taskId, String owner) {
        if (isHistoryLevelAtLeast(HistoryLevel.AUDIT)) {
            HistoricTaskInstanceEntity historicTaskInstance = getHistoricTaskInstanceEntityManager().findById(taskId);
            if (historicTaskInstance != null) {
                historicTaskInstance.setOwner(owner);
            }
        }
    }

    /*
     * (non-Javadoc)
     *
     * @see org.activiti.engine.impl.history.HistoryManagerInterface#recordTaskNameChange (java.lang.String, java.lang.String)
     */
    @Override
    public void recordTaskNameChange(String taskId, String taskName) {
        if (isHistoryLevelAtLeast(HistoryLevel.AUDIT)) {
            HistoricTaskInstanceEntity historicTaskInstance = getHistoricTaskInstanceEntityManager().findById(taskId);
            if (historicTaskInstance != null) {
                historicTaskInstance.setName(taskName);
            }
        }
    }

    /*
     * (non-Javadoc)
     *
     * @see org.activiti.engine.impl.history.HistoryManagerInterface# recordTaskDescriptionChange(java.lang.String, java.lang.String)
     */
    @Override
    public void recordTaskDescriptionChange(String taskId, String description) {
        if (isHistoryLevelAtLeast(HistoryLevel.AUDIT)) {
            HistoricTaskInstanceEntity historicTaskInstance = getHistoricTaskInstanceEntityManager().findById(taskId);
            if (historicTaskInstance != null) {
                historicTaskInstance.setDescription(description);
            }
        }
    }

    /*
     * (non-Javadoc)
     *
     * @see org.activiti.engine.impl.history.HistoryManagerInterface# recordTaskDueDateChange(java.lang.String, java.util.Date)
     */
    @Override
    public void recordTaskDueDateChange(String taskId, Date dueDate) {
        if (isHistoryLevelAtLeast(HistoryLevel.AUDIT)) {
            HistoricTaskInstanceEntity historicTaskInstance = getHistoricTaskInstanceEntityManager().findById(taskId);
            if (historicTaskInstance != null) {
                historicTaskInstance.setDueDate(dueDate);
            }
        }
    }

    /*
     * (non-Javadoc)
     *
     * @see org.activiti.engine.impl.history.HistoryManagerInterface# recordTaskPriorityChange(java.lang.String, int)
     */
    @Override
    public void recordTaskPriorityChange(String taskId, int priority) {
        if (isHistoryLevelAtLeast(HistoryLevel.AUDIT)) {
            HistoricTaskInstanceEntity historicTaskInstance = getHistoricTaskInstanceEntityManager().findById(taskId);
            if (historicTaskInstance != null) {
                historicTaskInstance.setPriority(priority);
            }
        }
    }

    /*
     * (non-Javadoc)
     *
     * @see org.activiti.engine.impl.history.HistoryManagerInterface# recordTaskCategoryChange(java.lang.String, java.lang.String)
     */
    @Override
    public void recordTaskCategoryChange(String taskId, String category) {
        if (isHistoryLevelAtLeast(HistoryLevel.AUDIT)) {
            HistoricTaskInstanceEntity historicTaskInstance = getHistoricTaskInstanceEntityManager().findById(taskId);
            if (historicTaskInstance != null) {
                historicTaskInstance.setCategory(category);
            }
        }
    }

    @Override
    public void recordTaskFormKeyChange(String taskId, String formKey) {
        if (isHistoryLevelAtLeast(HistoryLevel.AUDIT)) {
            HistoricTaskInstanceEntity historicTaskInstance = getHistoricTaskInstanceEntityManager().findById(taskId);
            if (historicTaskInstance != null) {
                historicTaskInstance.setFormKey(formKey);
            }
        }
    }

    /*
     * (non-Javadoc)
     *
     * @see org.activiti.engine.impl.history.HistoryManagerInterface# recordTaskParentTaskIdChange(java.lang.String, java.lang.String)
     */
    @Override
    public void recordTaskParentTaskIdChange(String taskId, String parentTaskId) {
        if (isHistoryLevelAtLeast(HistoryLevel.AUDIT)) {
            HistoricTaskInstanceEntity historicTaskInstance = getHistoricTaskInstanceEntityManager().findById(taskId);
            if (historicTaskInstance != null) {
                historicTaskInstance.setParentTaskId(parentTaskId);
            }
        }
    }

    /*
     * (non-Javadoc)
     *
     * @see org.activiti.engine.impl.history.HistoryManagerInterface# recordTaskExecutionIdChange(java.lang.String, java.lang.String)
     */
    @Override
    public void recordTaskExecutionIdChange(String taskId, String executionId) {
        if (isHistoryLevelAtLeast(HistoryLevel.AUDIT)) {
            HistoricTaskInstanceEntity historicTaskInstance = getHistoricTaskInstanceEntityManager().findById(taskId);
            if (historicTaskInstance != null) {
                historicTaskInstance.setExecutionId(executionId);
            }
        }
    }

    /*
     * (non-Javadoc)
     *
     * @see org.activiti.engine.impl.history.HistoryManagerInterface# recordTaskDefinitionKeyChange (org.activiti.engine.impl.persistence.entity.TaskEntity, java.lang.String)
     */
    @Override
    public void recordTaskDefinitionKeyChange(String taskId, String taskDefinitionKey) {
        if (isHistoryLevelAtLeast(HistoryLevel.AUDIT)) {
            HistoricTaskInstanceEntity historicTaskInstance = getHistoricTaskInstanceEntityManager().findById(taskId);
            if (historicTaskInstance != null) {
                historicTaskInstance.setTaskDefinitionKey(taskDefinitionKey);
            }
        }
    }

    /* (non-Javadoc)
     * @see org.activiti.engine.impl.history.HistoryManagerInterface#recordTaskProcessDefinitionChange(java.lang.String, java.lang.String)
     */
    @Override
    public void recordTaskProcessDefinitionChange(String taskId, String processDefinitionId) {
        if (isHistoryLevelAtLeast(HistoryLevel.ACTIVITY)) {
            HistoricTaskInstanceEntity historicTaskInstance = getHistoricTaskInstanceEntityManager().findById(taskId);
            if (historicTaskInstance != null) {
                historicTaskInstance.setProcessDefinitionId(processDefinitionId);
            }
        }
    }

    // Variables related history

    /*
     * (non-Javadoc)
     *
     * @see org.activiti.engine.impl.history.HistoryManagerInterface#recordVariableCreate (org.activiti.engine.impl.persistence.entity.VariableInstanceEntity)
     */
    @Override
    public void recordVariableCreate(VariableInstanceEntity variable) {
        // Historic variables
        if (isHistoryLevelAtLeast(HistoryLevel.ACTIVITY)) {
            getHistoricVariableInstanceEntityManager().copyAndInsert(variable);
        }
    }

    /*
     * (non-Javadoc)
     *
     * @see org.activiti.engine.impl.history.HistoryManagerInterface# recordHistoricDetailVariableCreate (org.activiti.engine.impl.persistence.entity.VariableInstanceEntity,
     * org.activiti.engine.impl.persistence.entity.ExecutionEntity, boolean)
     */
    @Override
    public void recordHistoricDetailVariableCreate(
        VariableInstanceEntity variable,
        ExecutionEntity sourceActivityExecution,
        boolean useActivityId
    ) {
        if (isHistoryLevelAtLeast(HistoryLevel.FULL)) {
            HistoricDetailVariableInstanceUpdateEntity historicVariableUpdate =
                getHistoricDetailEntityManager().copyAndInsertHistoricDetailVariableInstanceUpdateEntity(variable);

            if (useActivityId && sourceActivityExecution != null) {
                HistoricActivityInstanceEntity historicActivityInstance = findActivityInstance(
                    sourceActivityExecution,
                    false,
                    false
                );
                if (historicActivityInstance != null) {
                    historicVariableUpdate.setActivityInstanceId(historicActivityInstance.getId());
                }
            }
        }
    }

    /*
     * (non-Javadoc)
     *
     * @see org.activiti.engine.impl.history.HistoryManagerInterface#recordVariableUpdate (org.activiti.engine.impl.persistence.entity.VariableInstanceEntity)
     */
    @Override
    public void recordVariableUpdate(VariableInstanceEntity variable) {
        if (isHistoryLevelAtLeast(HistoryLevel.ACTIVITY)) {
            HistoricVariableInstanceEntity historicProcessVariable = getEntityCache().findInCache(
                HistoricVariableInstanceEntity.class,
                variable.getId()
            );
            if (historicProcessVariable == null) {
                historicProcessVariable =
                    getHistoricVariableInstanceEntityManager().findHistoricVariableInstanceByVariableInstanceId(
                        variable.getId()
                    );
            }

            if (historicProcessVariable != null) {
                getHistoricVariableInstanceEntityManager().copyVariableValue(historicProcessVariable, variable);
            } else {
                getHistoricVariableInstanceEntityManager().copyAndInsert(variable);
            }
        }
    }

    // Comment related history

    /*
     * (non-Javadoc)
     *
     * @see org.activiti.engine.impl.history.HistoryManagerInterface# createIdentityLinkComment(java.lang.String, java.lang.String, java.lang.String, java.lang.String, boolean)
     */
    @Override
    public void createIdentityLinkComment(String taskId, String userId, String groupId, String type, boolean create) {
        createIdentityLinkComment(taskId, userId, groupId, type, create, false);
    }

    @Override
    public void createUserIdentityLinkComment(String taskId, String userId, String type, boolean create) {
        createIdentityLinkComment(taskId, userId, null, type, create, false);
    }

    @Override
    public void createGroupIdentityLinkComment(String taskId, String groupId, String type, boolean create) {
        createIdentityLinkComment(taskId, null, groupId, type, create, false);
    }

    @Override
    public void createUserIdentityLinkComment(
        String taskId,
        String userId,
        String type,
        boolean create,
        boolean forceNullUserId
    ) {
        createIdentityLinkComment(taskId, userId, null, type, create, forceNullUserId);
    }

    /*
     * (non-Javadoc)
     *
     * @see org.activiti.engine.impl.history.HistoryManagerInterface# createIdentityLinkComment(java.lang.String, java.lang.String, java.lang.String, java.lang.String, boolean, boolean)
     */
    @Override
    public void createIdentityLinkComment(
        String taskId,
        String userId,
        String groupId,
        String type,
        boolean create,
        boolean forceNullUserId
    ) {
        if (isHistoryEnabled()) {
            String authenticatedUserId = Authentication.getAuthenticatedUserId();
            CommentEntity comment = getCommentEntityManager().create();
            comment.setUserId(authenticatedUserId);
            comment.setType(CommentEntity.TYPE_EVENT);
            comment.setTime(getClock().getCurrentTime());
            comment.setTaskId(taskId);
            if (userId != null || forceNullUserId) {
                if (create) {
                    comment.setAction(Event.ACTION_ADD_USER_LINK);
                } else {
                    comment.setAction(Event.ACTION_DELETE_USER_LINK);
                }
                comment.setMessage(new String[] { userId, type });
            } else {
                if (create) {
                    comment.setAction(Event.ACTION_ADD_GROUP_LINK);
                } else {
                    comment.setAction(Event.ACTION_DELETE_GROUP_LINK);
                }
                comment.setMessage(new String[] { groupId, type });
            }

            getCommentEntityManager().insert(comment);
        }
    }

    @Override
    public void createProcessInstanceIdentityLinkComment(
        String processInstanceId,
        String userId,
        String groupId,
        String type,
        boolean create
    ) {
        createProcessInstanceIdentityLinkComment(processInstanceId, userId, groupId, type, create, false);
    }

    @Override
    public void createProcessInstanceIdentityLinkComment(
        String processInstanceId,
        String userId,
        String groupId,
        String type,
        boolean create,
        boolean forceNullUserId
    ) {
        if (isHistoryEnabled()) {
            String authenticatedUserId = Authentication.getAuthenticatedUserId();
            CommentEntity comment = getCommentEntityManager().create();
            comment.setUserId(authenticatedUserId);
            comment.setType(CommentEntity.TYPE_EVENT);
            comment.setTime(getClock().getCurrentTime());
            comment.setProcessInstanceId(processInstanceId);
            if (userId != null || forceNullUserId) {
                if (create) {
                    comment.setAction(Event.ACTION_ADD_USER_LINK);
                } else {
                    comment.setAction(Event.ACTION_DELETE_USER_LINK);
                }
                comment.setMessage(new String[] { userId, type });
            } else {
                if (create) {
                    comment.setAction(Event.ACTION_ADD_GROUP_LINK);
                } else {
                    comment.setAction(Event.ACTION_DELETE_GROUP_LINK);
                }
                comment.setMessage(new String[] { groupId, type });
            }
            getCommentEntityManager().insert(comment);
        }
    }

    /*
     * (non-Javadoc)
     *
     * @see org.activiti.engine.impl.history.HistoryManagerInterface# createAttachmentComment(java.lang.String, java.lang.String, java.lang.String, boolean)
     */
    @Override
    public void createAttachmentComment(
        String taskId,
        String processInstanceId,
        String attachmentName,
        boolean create
    ) {
        if (isHistoryEnabled()) {
            String userId = Authentication.getAuthenticatedUserId();
            CommentEntity comment = getCommentEntityManager().create();
            comment.setUserId(userId);
            comment.setType(CommentEntity.TYPE_EVENT);
            comment.setTime(getClock().getCurrentTime());
            comment.setTaskId(taskId);
            comment.setProcessInstanceId(processInstanceId);
            if (create) {
                comment.setAction(Event.ACTION_ADD_ATTACHMENT);
            } else {
                comment.setAction(Event.ACTION_DELETE_ATTACHMENT);
            }
            comment.setMessage(attachmentName);
            getCommentEntityManager().insert(comment);
        }
    }

    // Identity link related history
    /*
     * (non-Javadoc)
     *
     * @see org.activiti.engine.impl.history.HistoryManagerInterface# recordIdentityLinkCreated (org.activiti.engine.impl.persistence.entity.IdentityLinkEntity)
     */
    @Override
    public void recordIdentityLinkCreated(IdentityLinkEntity identityLink) {
        // It makes no sense storing historic counterpart for an identity-link
        // that is related
        // to a process-definition only as this is never kept in history
        if (
            isHistoryLevelAtLeast(HistoryLevel.AUDIT) &&
            (identityLink.getProcessInstanceId() != null || identityLink.getTaskId() != null)
        ) {
            HistoricIdentityLinkEntity historicIdentityLinkEntity = getHistoricIdentityLinkEntityManager().create();
            historicIdentityLinkEntity.setId(identityLink.getId());
            historicIdentityLinkEntity.setGroupId(identityLink.getGroupId());
            historicIdentityLinkEntity.setProcessInstanceId(identityLink.getProcessInstanceId());
            historicIdentityLinkEntity.setTaskId(identityLink.getTaskId());
            historicIdentityLinkEntity.setType(identityLink.getType());
            historicIdentityLinkEntity.setUserId(identityLink.getUserId());
            historicIdentityLinkEntity.setDetails(identityLink.getDetails());
            getHistoricIdentityLinkEntityManager().insert(historicIdentityLinkEntity, false);
        }
    }

    /*
     * (non-Javadoc)
     *
     * @see org.activiti.engine.impl.history.HistoryManagerInterface# deleteHistoricIdentityLink(java.lang.String)
     */
    @Override
    public void deleteHistoricIdentityLink(String id) {
        if (isHistoryLevelAtLeast(HistoryLevel.AUDIT)) {
            getHistoricIdentityLinkEntityManager().delete(id);
        }
    }

    /*
     * (non-Javadoc)
     *
     * @see org.activiti.engine.impl.history.HistoryManagerInterface# updateProcessBusinessKeyInHistory (org.activiti.engine.impl.persistence.entity.ExecutionEntity)
     */
    @Override
    public void updateProcessBusinessKeyInHistory(ExecutionEntity processInstance) {
        if (isHistoryEnabled()) {
            if (log.isDebugEnabled()) {
                log.debug("updateProcessBusinessKeyInHistory : {}", processInstance.getId());
            }
            if (processInstance != null) {
                HistoricProcessInstanceEntity historicProcessInstance =
                    getHistoricProcessInstanceEntityManager().findById(processInstance.getId());
                if (historicProcessInstance != null) {
                    historicProcessInstance.setBusinessKey(processInstance.getProcessInstanceBusinessKey());
                    getHistoricProcessInstanceEntityManager().update(historicProcessInstance, false);
                }
            }
        }
    }

    @Override
    public void recordVariableRemoved(VariableInstanceEntity variable) {
        if (isHistoryLevelAtLeast(HistoryLevel.ACTIVITY)) {
            HistoricVariableInstanceEntity historicProcessVariable = getEntityCache().findInCache(
                HistoricVariableInstanceEntity.class,
                variable.getId()
            );
            if (historicProcessVariable == null) {
                historicProcessVariable =
                    getHistoricVariableInstanceEntityManager().findHistoricVariableInstanceByVariableInstanceId(
                        variable.getId()
                    );
            }

            if (historicProcessVariable != null) {
                getHistoricVariableInstanceEntityManager().delete(historicProcessVariable);
            }
        }
    }

    protected String parseActivityType(FlowElement element) {
        String elementType = element.getClass().getSimpleName();
        elementType = elementType.substring(0, 1).toLowerCase() + elementType.substring(1);
        return elementType;
    }

    protected EntityCache getEntityCache() {
        return getSession(EntityCache.class);
    }

    public HistoryLevel getHistoryLevel() {
        return historyLevel;
    }

    public void setHistoryLevel(HistoryLevel historyLevel) {
        this.historyLevel = historyLevel;
    }
}
